﻿using System;
using System.Collections.Generic;

using UnityEngine;
using BestHTTP;
using BestHTTP.SocketIO;
using BestHTTP.JSON;
using BestHTTP.SocketIO.Events;

public sealed class SocketIOChatSample : MonoBehaviour
{
    private readonly TimeSpan TYPING_TIMER_LENGTH = TimeSpan.FromMilliseconds(700);

    private enum ChatStates
    {
        Login,
        Chat
    }

    #region Fields

    /// <summary>
    /// The Socket.IO manager instance.
    /// </summary>
    private SocketManager Manager;

    /// <summary>
    /// Current state of the chat demo.
    /// </summary>
    private ChatStates State;

    /// <summary>
    /// The selected nickname
    /// </summary>
    private string userName = string.Empty;

    /// <summary>
    /// Currently typing message
    /// </summary>
    private string message = string.Empty;

    /// <summary>
    /// Sent and received messages.
    /// </summary>
    private string chatLog = string.Empty;

    /// <summary>
    /// Position of the scroller
    /// </summary>
    private Vector2 scrollPos;

    /// <summary>
    /// True if the user is currently typing
    /// </summary>
    private bool typing;

    /// <summary>
    /// When the message changed.
    /// </summary>
    private DateTime lastTypingTime = DateTime.MinValue;

    /// <summary>
    /// Users that typing.
    /// </summary>
    private List<string> typingUsers = new List<string>();

    #endregion

    #region Unity Events
        
    void Start()
    {
        // The current state is Login
        State = ChatStates.Login;

        // Change an option to show how it should be done
        SocketOptions options = new SocketOptions();
        options.AutoConnect = false;
        
        // Create the Socket.IO manager
        Manager = new SocketManager(new Uri("http://chat.socket.io/socket.io/"), options);

        // Set up custom chat events
        Manager.Socket.On("login", OnLogin);
        Manager.Socket.On("new message", OnNewMessage);
        Manager.Socket.On("user joined", OnUserJoined);
        Manager.Socket.On("user left", OnUserLeft);
        Manager.Socket.On("typing", OnTyping);
        Manager.Socket.On("stop typing", OnStopTyping);

        // The argument will be an Error object.
        Manager.Socket.On(SocketIOEventTypes.Error, (socket, packet, args) => Debug.LogError(string.Format("Error: {0}", args[0].ToString())));

        // We set SocketOptions' AutoConnect to false, so we have to call it manually.
        Manager.Open();
    }

    void OnDestroy()
    {
        // Leaving this sample, close the socket
        Manager.Close();
    }

    void Update()
    {
        // Go back to the demo selector
        if (Input.GetKeyDown(KeyCode.Escape))
            SampleSelector.SelectedSample.DestroyUnityObject();

        // Stop typing if some time passed without typing
        if (typing)
        {
            var typingTimer = DateTime.UtcNow;
            var timeDiff = typingTimer - lastTypingTime;
            if (timeDiff >= TYPING_TIMER_LENGTH)
            {
                Manager.Socket.Emit("stop typing");
                typing = false;
            }
        }
    }

    void OnGUI()
    {
        switch(State)
        {
            case ChatStates.Login: DrawLoginScreen(); break;
            case ChatStates.Chat: DrawChatScreen(); break;
        }
    }

    #endregion

    #region Chat Logic

    /// <summary>
    /// Called from an OnGUI event to draw the Login Screen.
    /// </summary>
    void DrawLoginScreen()
    {
        GUIHelper.DrawArea(GUIHelper.ClientArea, true, () =>
            {
                GUILayout.BeginVertical();
                    GUILayout.FlexibleSpace();

                    GUIHelper.DrawCenteredText("What's your nickname?");
                    userName = GUILayout.TextField(userName);

                    if (GUILayout.Button("Join"))
                        SetUserName();

                    GUILayout.FlexibleSpace();
                GUILayout.EndVertical();
            });
    }

    /// <summary>
    /// Called from an OnGUI event to draw the Chat Screen.
    /// </summary>
    void DrawChatScreen()
    {
        GUIHelper.DrawArea(GUIHelper.ClientArea, true, () =>
            {
                GUILayout.BeginVertical();
                    scrollPos = GUILayout.BeginScrollView(scrollPos);
                        GUILayout.Label(chatLog, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
                    GUILayout.EndScrollView();

                    string typing = string.Empty;

                    if (typingUsers.Count > 0)
                    {
                        typing += string.Format("{0}", typingUsers[0]);

                        for (int i = 1; i < typingUsers.Count; ++i)
                            typing += string.Format(", {0}", typingUsers[i]);

                        if (typingUsers.Count == 1)
                            typing += " is typing!";
                        else
                            typing += " are typing!";
                    }

                    GUILayout.Label(typing);

                    GUILayout.Label("Type here:");

                    GUILayout.BeginHorizontal();
                    message = GUILayout.TextField(message);

                    if (GUILayout.Button("Send", GUILayout.MaxWidth(100)))
                        SendMessage();
                    GUILayout.EndHorizontal();

                    if (GUI.changed)
                        UpdateTyping();

                GUILayout.EndVertical();
            });
    }

    void SetUserName()
    {
        if (string.IsNullOrEmpty(userName))
            return;

        State = ChatStates.Chat;

        Manager.Socket.Emit("add user", userName);
    }

    void SendMessage()
    {
        if (string.IsNullOrEmpty(message))
            return;

        Manager.Socket.Emit("new message", message);

        chatLog += string.Format("{0}: {1}\n", userName, message);
        message = string.Empty;
    }

    void UpdateTyping()
    {
        if (!typing)
        {
            typing = true;
            Manager.Socket.Emit("typing");
        }

        lastTypingTime = DateTime.UtcNow;
    }

    void addParticipantsMessage(Dictionary<string, object> data)
    {
        int numUsers = Convert.ToInt32(data["numUsers"]);

        if (numUsers == 1)
            chatLog += "there's 1 participant\n";
        else
            chatLog += "there are " + numUsers + " participants\n";
    }

    void addChatMessage(Dictionary<string, object> data)
    {
        var username = data["username"] as string;
        var msg = data["message"] as string;

        chatLog += string.Format("{0}: {1}\n", username, msg);
    }

    void AddChatTyping(Dictionary<string, object> data)
    {
        var username = data["username"] as string;

        typingUsers.Add(username);
    }

    void RemoveChatTyping(Dictionary<string, object> data)
    {
        var username = data["username"] as string;

        int idx = typingUsers.FindIndex((name) => name.Equals(username));
        if (idx != -1)
            typingUsers.RemoveAt(idx);
    }

    #endregion

    #region Custom SocketIO Events

    void OnLogin(Socket socket, Packet packet, params object[] args)
    {
        chatLog = "Welcome to Socket.IO Chat — \n";

        addParticipantsMessage(args[0] as Dictionary<string, object>);
    }

    void OnNewMessage(Socket socket, Packet packet, params object[] args)
    {
        addChatMessage(args[0] as Dictionary<string, object>);
    }

    void OnUserJoined(Socket socket, Packet packet, params object[] args)
    {
        var data = args[0] as Dictionary<string, object>;

        var username = data["username"] as string;

        chatLog += string.Format("{0} joined\n", username);

        addParticipantsMessage(data);
    }

    void OnUserLeft(Socket socket, Packet packet, params object[] args)
    {
        var data = args[0] as Dictionary<string, object>;

        var username = data["username"] as string;

        chatLog += string.Format("{0} left\n", username);

        addParticipantsMessage(data);
    }

    void OnTyping(Socket socket, Packet packet, params object[] args)
    {
        AddChatTyping(args[0] as Dictionary<string, object>);
    }

    void OnStopTyping(Socket socket, Packet packet, params object[] args)
    {
        RemoveChatTyping(args[0] as Dictionary<string, object>);
    }

    #endregion
}